import { mockReq } from '../utils/mockReq'

/*
有类似如下的需求
watch(obj, async () => {
  const res = await fetch("/something")
  finalData = res
})
若obj短时间多次改变，导致回调多次执行，由于无法控制fetch请求的返回时间，则无法控制finalData是最新的res数据
即此时出现了过期的副作用，因此，watch的回调函数还需要新增一个参数 (newVal, oldVal, onInvalidate)这样
watch(obj, async(newVal, oldVal, onInvalidate) => {
  let expired = false
  onInvalidate(() => expired = true)
  const res = await fetch("/something")
  if(!expired) {
    finalData = res
  }
})
*/
const bucket = new WeakMap()

let activeEffect
const effectStack = []
function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(effect)
    const res = fn()
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
    return res
  }
  effectFn.deps = []
  effectFn.options = options
  if (options.lazy) {
    return effectFn
  }
  effectFn()
}

function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  effectFn.deps.length = []
}

function track(target, key) {
  if (!activeEffect) return
  let depsMap = bucket.get(target)
  if (!depsMap) bucket.set(target, (depsMap = new Map()))
  let deps = depsMap.get(key)
  if (!deps) depsMap.set(key, (deps = new Set()))
  deps.add(activeEffect)
  activeEffect.deps.push(deps)
}

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const deps = depsMap.get(key)
  if (!deps) return
  const effectsToRun = new Set()
  deps.forEach((effectFn) => {
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })
  effectsToRun.forEach((effectFn) => {
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      effectFn()
    }
  })
}

// 计算属性
function computed(getter) {
  let dirty = true
  let val
  const effectFn = effect(getter, {
    lazy: true,
    scheduler() {
      dirty = true
      // effect中是信用computed值的时候，需要手动trigger
      trigger(obj, 'value')
    },
  })
  const obj = {
    get value() {
      if (dirty) {
        val = effectFn()
        dirty = false
      }
      // effect中使用computed值的时候，需要手动track
      track(obj, 'value')
      return val
    },
  }
  return obj
}

// watch
function watch(source, cb, options = {}) {
  let getter
  let newVal, oldVal
  let cleanup // 清除过期的回调
  function onInvalidate(fn) {
    cleanup = fn
  }
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  const job = () => {
    newVal = effectFn()
    // 调用回调之前， 先调用过期回调
    if (cleanup) cleanup()
    cb(newVal, oldVal, onInvalidate)
    oldVal = newVal
  }
  const effectFn = effect(
    () => {
      return getter()
    },
    {
      lazy: true,
      scheduler() {
        if (options.flush === 'post') {
          const p = Promise.resolve()
          p.then(job)
        } else {
          job()
        }
      },
    }
  )
  if (options.immediate) {
    job()
  } else {
    oldVal = effectFn()
  }
}

function traverse(value, seen = new Set()) {
  if (typeof value !== 'object' || value == null || seen.has(value)) return
  seen.add(value)
  for (const key in value) {
    traverse(value[key])
  }
  return value
}

const data = { foo: 0 }
const obj = new Proxy(data, {
  get(target, key) {
    track(target, key)
    return Reflect.get(target, key)
  },
  set(target, key, val) {
    Reflect.set(target, key, val)
    trigger(target, key)
    return true
  },
})

const computedFoo = computed(() => {
  console.count('computed执行')
  return obj.foo + 'hello'
})

obj.foo++
console.log(computedFoo.value)
console.log(computedFoo.value)
console.log(computedFoo.value)

watch(obj, async (newVal, oldVal, onInvalidate) => {
  let expired = false
  onInvalidate(() => {
    expired = true
  })

  const res = await mockReq(obj.foo)
  if (!expired) {
    console.log('not expired', res)
  }
})

obj.foo++
setTimeout(() => {
  obj.foo++
  obj.foo++
})
/*
解释下：
第一次foo++时， 调用async callback，因为mockReq耗时，等在这一步， 记为为expired1
第二次foo++, 再次调用async callback, 执行了上一次onInvalidate注册的cleanup， expired1 = true， 放弃第一次的结果, 第二次的expired2  mockReq耗时
第三次foo++, 再次调用async callback, 执行上一次cleanup， expired2 = true， 放弃了第二次的结果， mockReq耗时， 第三次expired3(false), 然后res回来， 完成，答应
*/
